import random
import csv
import os
import subprocess as sub

###########################################################################
# Multidimensional Pareto ranking classes
###########################################################################

MINIMIZE = -1
MAXIMIZE = 2

#_________________________________________________________________________
class Individual(object):
    def __init__(self):
        pass
#_________________________________________________________________________
class ScoreMeta(object):
    def __init__(self, name, type, label=None):
        self.name = name
        self.type = type
        self.label = label
    
    def is_minimize(self):
        return self.type == MINIMIZE
    
    def is_maximize(self):
        return self.type == MAXIMIZE
    
    
    def __repr__(self):
        if self.is_minimize():
            type = "MINIMIZE"
        else: type = "MAXIMIZE" 
        out = "|%s|%s|"%(self.name.center(14), type.center(12))
        return out
#_________________________________________________________________________
class RankingResult(object):
    def __init__(self, ind, rank):
        self.ind = ind
        self.rank = rank
#_________________________________________________________________________
class Pareto(object):
    def __init__(self):
        self.inds = []
        self.scores = {}
        self.unranked = []
    
    def set_inds(self, inds):
        self.inds = inds
    
    def set_scores(self, scores):
        """
        scores: List of ScoreMeta objects
        """
        self.scores = {}
        for score in scores:
            assert isinstance(score, ScoreMeta)
            self.scores[score.name] = score
                 
    def register_score(self, name, type, label=None):
        assert type == MINIMIZE or type == MAXIMIZE
        if self.scores.has_key(name):
            raise Exception("The score %s already exists"%name)
        self.scores[name] = ScoreMeta(name, type, label)

    def deregister_score(self, name):
        if self.scores.has_key(name):
            del self.scores[name]
        else: 
            raise Exception("The score %s doesn't exist"%name)

    def get_scores(self):
        return self.scores.values()

    def _dominates(self, ind1, ind2):
        equal = True
        for score in self.scores.values():
            assert isinstance(score, ScoreMeta)
            val1 = getattr(ind1, score.name)
            val2 = getattr(ind2, score.name)
            if val1 != val2: equal = False
            if score.is_minimize():
                if val2 < val1:
                    return False
            elif val2 > val1:
                return False
        if equal: return False
        return True
    
    def _on_pareto_front(self, ind, inds):
        for ind2 in inds:
            if self._dominates(ind2, ind):
                return False
        return True

    def _is_fully_evaluated(self, ind):
        for name in self.scores.keys():
            if not hasattr(ind, name):
                return False
            if getattr(ind, name) is None:
                return False
        return True

    def _extract_pareto_front(self, inds):
        pareto_front = []
        non_pareto_front = []
        for ind in inds:    
            if self._on_pareto_front(ind, inds):
                pareto_front.append(ind)
            else:
                non_pareto_front.append(ind)
        return pareto_front, non_pareto_front            

    def rank(self, max_level=None):
        rank = 1
        cnt = 0
        ranked = []
        inds = []
        for ind in self.inds:    
            if self._is_fully_evaluated(ind):
                inds.append(ind)
        while len(inds) > 0:
            pareto_front, non_pareto_front = self._extract_pareto_front(inds)
            #print "pareto", len(pareto_front), "non pareto", len(non_pareto_front)
            for ind in pareto_front:
                ranked.append(RankingResult(ind, rank))
            inds = non_pareto_front
            cnt +=1
            rank += 1
            if max_level and cnt == max_level: break
        self.unranked = inds
        return ranked
    
    def get_unranked(self):
        return self.unranked
        
###########################################################################
# Multidimensional Pareto ranking function
###########################################################################

def rank_inds(inds, score_metas):
    print "Writing inds to input.txt"
    f_temp = open("input.txt", "w")
    for ind in inds:
        f_temp.write("%s %s %s %s\n"%(ind.id, ind.pos[0], ind.pos[1], ind.pos[2]))
    f_temp.close()
    
    print "Ranking inds"
    command = os.path.join(os.getcwd(), "ranking.exe")
    sub.call(command)
    
    print "Reading from output.txt to assigne the ranks"
    f_temp = open("output.txt", "r")
    content = f_temp.read()
    f_temp.close()
    rows = content.split("\n")
    for index, ind in enumerate(inds):
        row = rows[index].split(" ")
        assert ind.id == int(row[0])
        ind.rank = int(row[1])
        ind.ff_rank = int(row[2])
        
###########################################################################
# Script to process a csv file
# Assumes a file with columns: id, birth, death, ...
###########################################################################
#_________________________________________________________________________
def get_total_births(inds):
    return int(inds[-1].id)
#_________________________________________________________________________
def to_int(str_value):
    if str_value == 'None':
        return None
    else:
        return int(str_value)
#_________________________________________________________________________
def to_float(str_value):
    if str_value == 'None':
        return None
    else:
        return float(str_value)
#_________________________________________________________________________
def dist_between_points(p1, p2):
    x_vals = sorted([p1[0], p2[0]])
    x_dist = x_vals[1]-x_vals[0]
    y_vals = sorted([p1[1], p2[1]])
    y_dist = y_vals[1]-y_vals[0]
    z_vals = sorted([p1[2], p2[2]])
    z_dist = z_vals[1]-z_vals[0]
    
    return (x_dist**2 + y_dist**2 + z_dist**2)**0.5
#_________________________________________________________________________


def read_file(file_path):
    f_in = open(file_path, 'r')
    raw_data = csv.reader(f_in, delimiter=',')
    data = []
    for row in raw_data:
        data.append(row)
    f_in.close()
    print "Got data"
    return data
    
def create_inds(data):
    header_1 = data[0]
    header_2 = data[1]
    score_names = header_1[3:]
    score_opts = header_2[3:]

    #get the scores
    score_metas = []
    assert len(score_names) == len(score_min_max)
    num_scores = len(score_names)
    for i in range(num_Scores):
        score_name = score_names[i]
        score_opt = score_opts[i]
        score_vals = [row[3+i] for row in data[2:] if data != "None"]
        score_domain = [min(score_vals), max(score_vals)]
        if score_min_max == "MIN":
            type = MINIMIZE
        else:
            type = MAXIMIZE
        score_meta = ScoreMeta(score_name, score_opt, score_domain)
        score_metas.append(score_meta)
    
    #get the inds
    inds = []
    for row in data[2:]:
        ind = Individual()
        ind.id   = to_int(row[0])
        ind.birth = to_int(row[1])
        ind.death  = to_int(row[2])
        ind.subrank = {}
        has_a_none_val = False
        for score_meta in score_meta, value in zip(score_names, row[3:]):
            if value != "None":
                setattr(ind, score_name, to_float(value))
            else:
                has_a_none_val = True
        if not has_a_none_val: inds.append(ind)
        
    #nornmalise the scores


        
    print "Created inds and score metas"
    return inds, score_metas
    

    
def normalise_scores(score_names, score_min_maxs, inds):
    sorted_lists = []
    for ind in inds:
        for score_name in score_names:
            sorted_lists.append(sorted([getattr(ind, score_name) for ind in inds]))
    for ind in inds:
        scores = []
        scores_normalised = []
        for i in range(len(score_names)):
            score_name = score_names[i]
            col_min_max = score_min_maxs[i]
            col_sorted_list = sorted_lists[i]
            score = getattr(ind, score_name)
            score_range = sorted_lists[-1] - sorted_lists[0]
            score_normalised = (score - sorted_lists[0]) / score_range
            if col_min_max < 0:
                score_normalised = 1 - score_normalised
            scores.append(score)
            scores_normalised.append(score_normalised)
            setattr(ind, score_name+"_norm", score_normalised)
        ind.scores = scores
        ind.scores_normalised = scores_normalised

def rank(inds, score_metas):
    ranking = Pareto()
    ranking.set_scores(score_metas)
    rank_inds(inds)
    
    
def rank_susets(inds, score_metas):
    ranking = Pareto()
    ranking.set_scores(score_metas)
    step = 100
    key = 0
    total_births = get_total_births(inds)
    keys = []
    while key < total_births:
        print key
        key = key + step
        keys.append(key)
        subset = [ind for ind in inds if ind.id < key]
        others = [ind for ind in inds if ind.id >= key]
        print "Ranking subset size", len(subset)
        #ranking.set_inds(subset)
        #ranked_inds = ranking.rank()
        rank_inds(subset)
        print "Ranking done."
        for ind in subset:
            ind.subrank[key] = ind.rank
        for ind in others:
            ind.subrank[key] = None 
    return keys
    
"""
    
    print "Write all normailsed points to file"
    f_norm_all_points = open(file_path[:-4] + "_norm_all_points.csv", 'w')
    f_norm_all_points.write("X,Y,Z,ID,Rank,FF_Rank\n")

    for ind in inds:
        scores_normalised = [str(i) for i in ind.scores_normalised]
        f_norm_all_points.write("%s,%s,%s,%s\n"%(",".join(scores_normalised), ind.id, ind.rank, ind.ff_rank))
    f_norm_all_points.close()


    print "Write pareto subsets set points to file"
    f_points = open(file_path[:-4] + "_points.csv", 'w')
    f_points.write("X,Y,Z,Pareto_Set\n")
    for key in keys:
        pareto_set = [ind for ind in inds if ind.subrank[key] == 1]
        points = [ind.scores_normalised for ind in pareto_set]
        points.sort()
        #write points to file
        for point in points:
            f_points.write(str(point[0])+","+str(point[1])+","+str(point[2])+","+str(key)+"\n")    
    f_points.close()
    


    print "Write subranks to file"
    ## write the results
    f_out = open(file_path[:-4] + "_ranked.csv", 'w')
    score_cols = ", ".join([col_a, col_b, col_c, col_a+norm, col_b+norm, col_c+norm])
    rank_cols = ", ".join(["rank_" + str(key) for key in keys])
    f_out.write("" + str(id) + ", " + str(birth) + ", " + str(death) + ", " + score_cols + ", " + rank_cols + "\n")
    for ind in inds:
        f_out.write(str(ind.id) + "," +\
                        str(ind.birth) + "," +\
                        str(ind.death) + "," +\
                        str(getattr(ind, col_a)) + "," +\
                        str(getattr(ind, col_b)) + "," +\
                        str(getattr(ind, col_c)) + "," +\
                        str(getattr(ind, col_a+norm)) + "," +\
                        str(getattr(ind, col_b+norm)) + "," +\
                        str(getattr(ind, col_c+norm)) + "," +\
                        ", ".join([str(val) for val in ind.subrank.values()]) + "\n")
    f_out.close()


    print "Calculate distance between pareto fronts"
    f_dist_metric = open(file_path[:-4] + "_dist_metric.csv", 'w')
    f_dist_metric.write("pareto_set,dist\n")
    distances = []
    for i, key in enumerate(keys[:-1]):
        pareto_set_a = [ind for ind in inds if ind.subrank[key] == 1]
        pareto_set_b = [ind for ind in inds if ind.subrank[keys[i+1]] == 1]
        total_distance = 0
        for i_a in pareto_set_a:
            dists = [dist_between_points(i_a.scores_normalised, i_b.scores_normalised) for i_b in pareto_set_b]
            dists.sort()
            closest = dists[0]
            total_distance += closest
        distances.append(total_distance)
        f_dist_metric.write(str(key)+","+str(total_distance)+"\n")
    f_dist_metric.close()

    
    print "Calculate hypervolumes"
    f_hv_metric = open(file_path[:-4] + "_hv_metric.csv", 'w')
    f_hv_metric.write("pareto_set,hv\n")
    hypervolumes = []
    command = os.path.join(os.getcwd(),"hv.exe")
    hv_args = [command, "-r", "1.01 1.01 1.01", "f_hv_in.txt"]
    for key in keys:
        pareto_set = [ind for ind in inds if ind.subrank[key] == 1]
        points = [ind.scores_normalised for ind in pareto_set]
        points.sort()
        #write points to file
        f_hv_in  = open('f_hv_in.txt', 'w')
        for point in points:
            f_hv_in.write(str(point[0])+" "+str(point[1])+" "+str(point[2])+"\n")
        f_hv_in.close()
        #calc hypervolume
        p = sub.Popen(hv_args, stdout=sub.PIPE)
        hv = p.communicate()[0]
        hv = float(hv.strip())
        print "hypervolume_"+str(key)+" = " + str(hv)
        f_hv_metric.write(str(key)+","+str(hv)+"\n")
    f_hv_metric.close()
"""
#_________________________________________________________________________
"""
if __name__ == "__main__":
    import sys
    if len(sys.argv) != 2:
        print "Usage: python ranking.py ./a/b/c/file.csv"
    else:
        run_from_file(sys .argv[1])
"""
raw_data = read_file("individuals.csv")
inds, score_metas = create_inds(raw_data)
ranked = rank(inds, score_metas)



